﻿#region License
// Copyright 2010 Buu Nguyen (http://www.buunguyen.net/blog)
// 
// Licensed under the Apache License, Version 2.0 (the "License"); 
// you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at 
// 
// http://www.apache.org/licenses/LICENSE-2.0 
// 
// Unless required by applicable law or agreed to in writing, software 
// distributed under the License is distributed on an "AS IS" BASIS, 
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
// See the License for the specific language governing permissions and 
// limitations under the License.
// 
// The latest version of this file can be found at http://www.codeplex.com/aspmvcvalidation
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
using Mvc.Validation.Validators;

namespace Mvc.Validation
{
    /// <summary>
    /// Class to generate corresponding client-side validation code for an VAB-annotated
    /// type.
    /// 
    /// The generated JavaScript code targets the jQuery validation plugin:
    /// http://bassistance.de/jquery-plugins/jquery-plugin-validation/.
    /// </summary>
    internal sealed class JQueryCodeGenerator : IValidationCodeGenerator
    {
        #region Implementing IValidationCodeGenerator
        public string GenerateCode<T>(string formName, bool ignoreMissingElements, 
            string readyFunctionName, string validateFunctionName)
        {
            var body = GenerateScriptBody<T>(ignoreMissingElements);
            return GenerateScriptWrapper(body, formName, readyFunctionName, validateFunctionName);
        }
        #endregion


        #region Private Members
        /// <summary>
        /// Generates the body of the script by inspecting all properties and their
        /// attributes.
        /// </summary>
        private static string GenerateScriptBody<T>(bool ignoreMissingElements)
        {
            var script = new StringBuilder();
            var props = TypeDescriptor.GetProperties(typeof(T));
            foreach (PropertyDescriptor prop in props)
            {
                var rules = GetRulesOfProperty(prop);
                var propertyScript = GenerateScriptForRules<T>(prop, rules, ignoreMissingElements);
                script.Append(propertyScript).AppendLine().AppendLine();
            }
            return script.ToString();
        }

        private static PropertyDescriptor GetProperty<T>(string name)
        {
            var props = TypeDescriptor.GetProperties(typeof(T));
            return props.Find(name, false);
        }

        /// <summary>
        /// Generates the wrapper and initialization JS code for the validation.
        /// </summary>
        private static string GenerateScriptWrapper(string bodyScript, string formName, 
            string validateFunctionName, string readyFunctionName)
        {
            var script = new StringBuilder();

            readyFunctionName = string.IsNullOrEmpty(readyFunctionName) 
                ? "$().ready"
                : readyFunctionName;
            script.Append(readyFunctionName).AppendFormat("(function() {0}", "{").AppendLine().AppendLine();

            if (!string.IsNullOrEmpty(formName))
            {
                if (string.IsNullOrEmpty(validateFunctionName))
                    script.AppendFormat("$(\"#{0}\").validate();", formName).AppendLine();
                else
                    script.AppendFormat("{0}(\"#{1}\");", validateFunctionName, formName).AppendLine();
            }

            script.AppendLine().Append(bodyScript);

            script.AppendFormat("{0});", "}");
            return string.Format("<script type=\"text/javascript\"><!--{1}{0}{1}--></script>",
                script, Environment.NewLine);
        }

        /// <summary>
        /// Retrieves all the validation attributes for the specified <param name="prop"/>
        /// </summary>
        private static IList<BaseValidationAttribute> GetRulesOfProperty(MemberDescriptor prop)
        {
            return prop.Attributes.OfType<BaseValidationAttribute>().ToList();
        }

        /// <summary>
        /// Generates the validation script for the specified <paramref name="prop"/>
        /// </summary>
        private static string GenerateScriptForRules<T>(MemberDescriptor prop,
            IList<BaseValidationAttribute> rules, bool ignoreMissingElements)
        {
            if (rules.Count == 0)
                return string.Empty;

            var rulesStr = new StringBuilder();
            var messagesStr = new StringBuilder();
            var clientId = GetClientID(prop);

            if (ignoreMissingElements)
            {
                rulesStr.AppendFormat("if($(\"#{0}\").length > 0) {1}",
                               clientId, "{").AppendLine();
            }

            rulesStr.AppendFormat("$(\"#{0}\").rules(\"add\", {1}",
                               clientId, "{").AppendLine();

            messagesStr.AppendFormat("\tmessages: {0}", "{").AppendLine();

            for (var i = 0; i < rules.Count; i++)
            {
                ClientScriptInfo scriptInfo = GetClientScriptInfo<T>(rules[i]);
                rulesStr.AppendFormat("\t{0},", scriptInfo.Rule).AppendLine();
                messagesStr.AppendFormat("\t\t{0}", scriptInfo.Message);

                if (i < rules.Count - 1)
                {
                    messagesStr.Append(",");
                }
                messagesStr.AppendLine();
            }
            messagesStr.AppendFormat("\t{0}", "}").AppendLine();
            rulesStr.Append(messagesStr.ToString());
            rulesStr.AppendFormat("{0});", "}");

            if (ignoreMissingElements)
            {
                rulesStr.AppendFormat("{0}", "}");
            }
            return rulesStr.ToString();
        }

        private static string GetClientID(MemberDescriptor prop)
        {
            var clientIdList = prop.Attributes.OfType<ClientIDAttribute>().ToList();
            return clientIdList.Count == 0 
                ? prop.Name.ToLower() 
                : clientIdList[0].ClientId;
        }

        private static ClientScriptInfo GetClientScriptInfo<T>(BaseValidationAttribute rule)
        {
            #region Custom Attribute
            if (rule is CustomValidatorAttribute)
            {
                var customRule = rule as CustomValidatorAttribute;
                return customRule.GetClientScriptInfo();
            }
            #endregion

            #region Built-in VAB Attributes
            if (rule is NotNullValidatorAttribute)
            {
                return new ClientScriptInfo
                {
                    Rule = "required: true",
                    Message = string.Format("required: \"{0}\"", rule.MessageTemplate)
                };
            }

            if (rule is StringLengthValidatorAttribute)
            {
                // lowerBound and upperBound fields are not visible
                // thus need to use reflection to retrieve them
                var lowerBound = GetField<int>(rule, "lowerBound");
                var upperBound = GetField<int>(rule, "upperBound");

                // To understand the magic numbers 3 & 5, refer here
                // http://msdn.microsoft.com/en-us/library/cc511854.aspx
                var errorMessage = rule.MessageTemplate
                    .Replace("{3}", lowerBound.ToString())
                    .Replace("{5}", upperBound.ToString());

                return new ClientScriptInfo
                    {
                        Rule = string.Format("minlength: \"{0}\", maxlength : \"{1}\"",
                                                  lowerBound, upperBound),
                        Message = string.Format("minlength: \"{0}\", maxlength : \"{0}\"",
                                                  errorMessage)
                    };
            }

            if (rule is PropertyComparisonValidatorAttribute)
            {
                var propToCompare = GetField<string>(rule, "propertyToCompare");
                var propDescriptor = GetProperty<T>(propToCompare);
                var propOperator = GetField<ComparisonOperator>(rule, "comparisonOperator");
                if (propOperator == ComparisonOperator.Equal)
                {
                    return new ClientScriptInfo
                    {
                        Rule = string.Format("equalTo: \"#{0}\"", GetClientID(propDescriptor)),
                        Message = string.Format("equalTo: \"{0}\"", rule.MessageTemplate)
                    };
                }
            }
            #endregion

            throw new NotSupportedException("Not supported attribute of type " + rule.GetType());
        }

        private static T GetField<T>(object obj, string fieldName)
        {
            var fieldInfo = obj.GetType().GetField(fieldName, 
                BindingFlags.Instance | BindingFlags.NonPublic);
            return (T)fieldInfo.GetValue(obj);
        }
        #endregion
    }
}
